function App() {
    const conf = {
        el: 'canvas',
        fov: 75,
        cameraZ: 100,
    };

    let renderer, scene, camera, cameraCtrl;
    let width, height, cx, cy, wWidth, wHeight;

    let ripple;
    let gridWWidth, gridWHeight;
    let gridWidth, gridHeight;

    const mouse = new THREE.Vector2();
    const mousePlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
    const mousePosition = new THREE.Vector3();
    const raycaster = new THREE.Raycaster();
    let mouseOver = false;

    init();

    function init() {
        // const gl = renderer.getContext();
        // const floatTextures = gl.getExtension('OES_texture_float');
        // if (!floatTextures) {
        //   alert('no floating point texture support');
        //   return;
        // }

        renderer = new THREE.WebGLRenderer({ canvas: document.getElementById(conf.el), antialias: true });
        camera = new THREE.PerspectiveCamera(conf.fov);
        camera.position.z = conf.cameraZ;

        updateSize();
        window.addEventListener('resize', updateSize, false);

        // gridWHeight = wHeight - 20;
        // gridWWidth = gridWHeight;
        gridWHeight = wHeight;
        gridWWidth = wWidth;
        gridWidth = gridWWidth * width / wWidth;
        gridHeight = gridWHeight * height / wHeight;

        ripple = new RippleEffect(renderer, width, height);

        const getGridMP = function (e) {
            const v = new THREE.Vector3();
            camera.getWorldDirection(v);
            v.normalize();
            mouse.x = ((e.clientX / width) * 2 - 1);
            mouse.y = (-(e.clientY / height) * 2 + 1);
            raycaster.setFromCamera(mouse, camera);
            raycaster.ray.intersectPlane(mousePlane, mousePosition);
            return { x: 2 * mousePosition.x / gridWWidth, y: 2 * mousePosition.y / gridWHeight };
        };

        renderer.domElement.addEventListener('mousemove', e => {
            mouseOver = true;
            const gp = getGridMP(e);
            ripple.addDrop(gp.x, gp.y, 0.05, 0.1);
        });
        renderer.domElement.addEventListener('mouseleave', e => { mouseOver = false; });

        // renderer.domElement.addEventListener('mouseup', e => {
        //   const gp = getGridMP(e);
        //   ripple.addDrop(gp.x, gp.y, 0.2, -3.0);
        // });

        initScene();
        animate();
    }

    function initScene() {
        scene = new THREE.Scene();

        let pointLight1 = new THREE.PointLight(0xFFFF80);
        pointLight1.position.set(-wWidth / 2, wHeight / 2, 50);
        scene.add(pointLight1);

        let pointLight2 = new THREE.PointLight(0xde3578);
        pointLight2.position.set(wWidth / 2, wHeight / 2, 50);
        scene.add(pointLight2);

        let pointLight3 = new THREE.PointLight(0xFF4040);
        pointLight3.position.set(-wWidth / 2, -wHeight / 2, 50);
        scene.add(pointLight3);

        let pointLight4 = new THREE.PointLight(0x0247e5);
        pointLight4.position.set(wWidth / 2, -wHeight / 2, 50);
        scene.add(pointLight4);

        renderer.domElement.addEventListener('mouseup', e => {
            pointLight1.color = new THREE.Color(chroma.random().hex());
            pointLight2.color = new THREE.Color(chroma.random().hex());
            pointLight3.color = new THREE.Color(chroma.random().hex());
            pointLight4.color = new THREE.Color(chroma.random().hex());
        });

        const material = new THREE.MeshStandardMaterial({ color: 0xffffff, side: THREE.DoubleSide, metalness: 0.5, roughness: 0.5, onBeforeCompile: shader => {
                shader.uniforms.hmap = { value: ripple.hMap.texture };
                shader.vertexShader = "uniform sampler2D hmap;\n" + shader.vertexShader;
                const token = '#include <begin_vertex>';
                const customTransform = `
        vec3 transformed = vec3(position);
        vec4 info = texture2D(hmap, uv);
        vNormal = vec3(info.b, sqrt(1.0 - dot(info.ba, info.ba)), info.a).xzy;
        transformed.z = 20. * info.r;
      `;
                shader.vertexShader = shader.vertexShader.replace(token, customTransform);
            } });

        let nx = Math.round(gridWidth / 5), ny = Math.round(gridHeight / 40);
        let dx = gridWWidth / nx, dy = gridWHeight / ny;
        for (let j = 0; j <= ny; j++) {
            const geometry = new THREE.BufferGeometry();
            const positions = [], uvs = [];
            const y = - gridWHeight / 2 + j * dy;
            for (let i = 0; i <= nx; i++) {
                positions.push(- gridWWidth / 2 + i * dx, y, 0);
                uvs.push(i / nx, j / ny);
            }
            geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
            geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
            geometry.computeBoundingSphere();
            scene.add(new THREE.Line(geometry, material));
        }

        nx = Math.round(gridWidth / 40); ny = Math.round(gridHeight / 5);
        dx = gridWWidth / nx; dy = gridWHeight / ny;
        for (let i = 0; i <= nx; i++) {
            const geometry = new THREE.BufferGeometry();
            const positions = [], uvs = [];
            const x = - gridWWidth / 2 + i * dx;
            for (let j = 0; j <= ny; j++) {
                positions.push(x, - gridWHeight / 2 + j * dy, 0);
                uvs.push(i / nx, j / ny);
            }
            geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
            geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
            geometry.computeBoundingSphere();
            scene.add(new THREE.Line(geometry, material));
        }
        camera.position.set(0, -gridWHeight/2, 40);
        camera.lookAt(new THREE.Vector3(0, -gridWHeight/6, 0));

        cameraCtrl = new THREE.OrbitControls(camera, renderer.domElement);
        cameraCtrl.enableDamping = true;
        cameraCtrl.dampingFactor = 0.1;
        cameraCtrl.rotateSpeed = 0.5;
    }

    function animate() {
        if (!mouseOver) {
            const time = Date.now() * 0.001;
            const x = Math.cos(time) * 0.2;
            const y = Math.sin(time) * 0.2;
            ripple.addDrop(x, y, 0.05, -0.04);
        }

        ripple.update();
        renderer.render(scene, camera);
        requestAnimationFrame(animate);
    }

    function updateSize() {
        width = window.innerWidth; cx = width / 2;
        height = window.innerHeight; cy = height / 2;
        renderer.setSize(width, height);
        camera.aspect = width / height;
        camera.updateProjectionMatrix();
        const wsize = getRendererSize();
        wWidth = wsize[0]; wHeight = wsize[1];
    }

    function getRendererSize() {
        const cam = new THREE.PerspectiveCamera(camera.fov, camera.aspect);
        const vFOV = (cam.fov * Math.PI) / 180;
        const height = 2 * Math.tan(vFOV / 2) * Math.abs(conf.cameraZ);
        const width = height * cam.aspect;
        return [width, height];
    }
}

const RippleEffect = (function () {
    function RippleEffect(renderer, width, height) {
        this.renderer = renderer;
        this.width = 512;
        this.height = 512;
        // this.delta = new THREE.Vector2(this.width / Math.pow(width, 2), this.height / Math.pow(height, 2));
        this.delta = new THREE.Vector2(1 / this.width, 1 / this.height);

        this.hMap = new THREE.WebGLRenderTarget(this.width, this.height, { type: THREE.FloatType, depthBuffer: false, stencilBuffer: false });
        this.hMap1 = new THREE.WebGLRenderTarget(this.width, this.height, { type: THREE.FloatType, depthBuffer: false, stencilBuffer: false });
        this.fsQuad = new FullScreenQuad();

        this.initShaders();
    }

    RippleEffect.prototype.initShaders = function () {
        // default vertex shader
        const defaultVertexShader = `
      varying vec2 vUv;
      void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `;

        this.copyMat = new THREE.ShaderMaterial({
            uniforms: { 'tDiffuse': { value: null } },
            vertexShader: defaultVertexShader,
            fragmentShader: `
        uniform sampler2D tDiffuse;
        varying vec2 vUv;
        void main() {
          gl_FragColor = texture2D(tDiffuse, vUv);
        }
      `,
        });

        this.updateMat = new THREE.ShaderMaterial({
            uniforms: {
                'tDiffuse': { value: null },
                'delta': new THREE.Uniform(this.delta),
            },
            vertexShader: defaultVertexShader,
            fragmentShader: `
        uniform sampler2D tDiffuse;
        uniform vec2 delta;
        varying vec2 vUv;
        void main() {
          vec4 texel = texture2D(tDiffuse, vUv);

          vec2 dx = vec2(delta.x, 0.0);
          vec2 dy = vec2(0.0, delta.y);
          float average = (
            texture2D(tDiffuse, vUv - dx).r +
            texture2D(tDiffuse, vUv - dy).r +
            texture2D(tDiffuse, vUv + dx).r +
            texture2D(tDiffuse, vUv + dy).r
          ) * 0.25;
          texel.g += (average - texel.r) * 2.0;
          texel.g *= 0.995;
          texel.r += texel.g;

          gl_FragColor = texel;
        }
      `,
        });

        this.normalsMat = new THREE.ShaderMaterial({
            uniforms: {
                'tDiffuse': { value: null },
                'delta': new THREE.Uniform(this.delta),
            },
            vertexShader: defaultVertexShader,
            fragmentShader: `
      uniform sampler2D tDiffuse;
      uniform vec2 delta;
      varying vec2 vUv;
      void main() {
        vec4 texel = texture2D(tDiffuse, vUv);
        vec3 dx = vec3(delta.x, texture2D(tDiffuse, vec2(vUv.x + delta.x, vUv.y)).r - texel.r, 0.0);
        vec3 dy = vec3(0.0, texture2D(tDiffuse, vec2(vUv.x, vUv.y + delta.y)).r - texel.r, delta.y);
        texel.ba = normalize(cross(dy, dx)).xz;
        gl_FragColor = texel;
      }
    `,
        });

        this.dropMat = new THREE.ShaderMaterial({
            uniforms: {
                'tDiffuse': { value: null },
                'center': new THREE.Uniform(new THREE.Vector2()),
                'radius': { value: 0.05 },
                'strength': { value: 0.5 },
            },
            vertexShader: defaultVertexShader,
            fragmentShader: `
        const float PI = 3.1415926535897932384626433832795;
        uniform sampler2D tDiffuse;
        uniform vec2 center;
        uniform float radius;
        uniform float strength;
        varying vec2 vUv;
        void main() {
          vec4 texel = texture2D(tDiffuse, vUv);
          float drop = max(0.0, 1.0 - length(center * 0.5 + 0.5 - vUv) / radius);
          drop = 0.5 - cos(drop * PI) * 0.5;
          texel.r += drop * strength;
          // texel.r = clamp(texel.r, -2.0, 2.0);
          gl_FragColor = texel;
        }
      `,
        });
    };

    RippleEffect.prototype.update = function () {
        this.updateHMap();
        this.updateHMapNormals();
    };

    RippleEffect.prototype.updateHMap = function () {
        this.updateMat.uniforms.tDiffuse.value = this.hMap.texture;
        this.renderShaderMat(this.updateMat, this.hMap1);
        this.swapBuffers();
    };

    RippleEffect.prototype.updateHMapNormals = function () {
        this.normalsMat.uniforms.tDiffuse.value = this.hMap.texture;
        this.renderShaderMat(this.normalsMat, this.hMap1);
        this.swapBuffers();
    };

    RippleEffect.prototype.addDrop = function (x, y, radius, strength) {
        this.dropMat.uniforms.tDiffuse.value = this.hMap.texture;
        this.dropMat.uniforms.center.value.set(x, y);
        this.dropMat.uniforms.radius.value = radius;
        this.dropMat.uniforms.strength.value = strength;
        this.renderShaderMat(this.dropMat, this.hMap1);
        this.swapBuffers();
    };

    RippleEffect.prototype.renderBuffer = function (buffer, target) {
        target = target ? target : null;
        this.copyMat.uniforms.tDiffuse.value = buffer.texture;
        this.renderShaderMat(this.copyMat, target);
    };

    RippleEffect.prototype.renderShaderMat = function (mat, target) {
        this.fsQuad.material = mat;
        const oldTarget = this.renderer.getRenderTarget();
        this.renderer.setRenderTarget(target);
        this.fsQuad.render(this.renderer);
        this.renderer.setRenderTarget(oldTarget);
    };

    RippleEffect.prototype.swapBuffers = function () {
        const temp = this.hMap;
        this.hMap = this.hMap1;
        this.hMap1 = temp;
    };

    // from https://threejs.org/examples/js/postprocessing/EffectComposer.js
    const FullScreenQuad = (function () {
        const camera = new THREE.OrthographicCamera(- 1, 1, 1, - 1, 0, 1);
        const geometry = new THREE.PlaneBufferGeometry(2, 2);

        const FullScreenQuad = function (material) {
            this._mesh = new THREE.Mesh(geometry, material);
        };

        Object.defineProperty(FullScreenQuad.prototype, 'material', {
            get: function () { return this._mesh.material; },
            set: function (value) { this._mesh.material = value; }
        });

        Object.assign(FullScreenQuad.prototype, {
            render: function (renderer) {
                renderer.render(this._mesh, camera);
            }
        });

        return FullScreenQuad;
    })();

    return RippleEffect;
})();

App();